1 /**
2 Copyright: Copyright (c) 2018, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This file contains an analyzer that uses clang-tidy.
7 */
8 module code_checker.engine.builtin.clang_tidy;
9 
10 import logger = std.experimental.logger;
11 import std.algorithm : copy, map, joiner, filter, among;
12 import std.array : appender, array, empty;
13 import std.concurrency : Tid, thisTid;
14 import std.exception : collectException;
15 import std.file : exists;
16 import std.format : format;
17 import std.path : buildPath;
18 import std.process : spawnProcess, wait;
19 import std.range : put, only, enumerate;
20 import std.typecons : Tuple;
21 
22 import colorlog;
23 import my.path : AbsolutePath;
24 
25 import code_checker.cli : Config;
26 import code_checker.engine.builtin.clang_tidy_classification : CountErrorsResult;
27 import code_checker.engine.file_filter;
28 import code_checker.engine.types;
29 import code_checker.process : RunResult;
30 
31 @safe:
32 
33 class ClangTidy : BaseFixture {
34     private {
35         Environment env;
36         Result result_;
37         string[] tidyArgs;
38     }
39 
40     override string name() {
41         return "clang-tidy";
42     }
43 
44     override string explain() {
45         return "using clang-tidy";
46     }
47 
48     /// The environment the analyzers execute in.
49     override void putEnv(Environment v) {
50         this.env = v;
51     }
52 
53     /// Setup the environment for analyze.
54     override void setup() {
55         import std.conv : text;
56         import code_checker.engine.builtin.clang_tidy_classification : filterSeverity,
57             diagnosticSeverity;
58         import code_checker.utility : replaceConfigWords;
59 
60         const systemConf = AbsolutePath(only(env.conf.clangTidy.systemConfig)
61                 .replaceConfigWords.front);
62 
63         auto app = appender!(string[])();
64         app.put(env.conf.clangTidy.binary);
65 
66         app.put("-p=.");
67 
68         if (env.conf.clangTidy.applyFixit) {
69             app.put(["--fix"]);
70         } else if (env.conf.clangTidy.applyFixitErrors) {
71             app.put(["--fix-errors"]);
72         }
73 
74         if (!env.conf.clangTidy.checkExtensions.empty)
75             ["--checks", env.conf.clangTidy.checkExtensions.joiner(",").text].copy(app);
76 
77         env.conf.compiler.extraFlags.map!(a => ["--extra-arg", a]).joiner.copy(app);
78 
79         ["--header-filter", env.conf.clangTidy.headerFilter].copy(app);
80 
81         if (exists(ClangTidyConstants.confFile)
82                 && !isCodeCheckerConfig(AbsolutePath(ClangTidyConstants.confFile))) {
83             logger.infof("Using local '%s' config", ClangTidyConstants.confFile);
84 
85             if (env.conf.staticCode.severity != typeof(env.conf.staticCode.severity).min) {
86                 logger.warningf("--severity do not work when using a local '%s'",
87                         ClangTidyConstants.confFile);
88             }
89         } else {
90             logger.tracef("Writing to %s using %s", ClangTidyConstants.confFile, systemConf);
91             writeClangTidyConfig(systemConf, env.conf);
92         }
93 
94         tidyArgs = app.data;
95     }
96 
97     /// Execute the analyzer.
98     override void execute() {
99         if (env.conf.clangTidy.applyFixit || env.conf.clangTidy.applyFixitErrors) {
100             executeFixit(env, tidyArgs, result_);
101         } else {
102             executeParallel(env, tidyArgs, result_);
103         }
104     }
105 
106     /// Cleanup after analyze.
107     override void tearDown() {
108     }
109 
110     /// Returns: the result of the analyzer.
111     override Result result() {
112         return result_;
113     }
114 }
115 
116 struct ExpectedReplyCounter {
117     int expected;
118     int replies;
119 
120     bool isWaitingForReplies() {
121         return replies < expected;
122     }
123 }
124 
125 void executeParallel(Environment env, string[] tidyArgs, ref Result result_) @safe {
126     import core.time : dur;
127     import std.concurrency : Tid, thisTid, receiveTimeout;
128     import std.format : format;
129     import std.parallelism : task, TaskPool;
130     import code_checker.engine.compile_db;
131     import code_checker.engine.logger : Logger;
132 
133     bool logged_failure;
134     auto logg = Logger(env.conf.logg.dir);
135     ExpectedReplyCounter cond;
136 
137     void handleResult(immutable(TidyResult)* res_) @trusted nothrow {
138         import std.format : format;
139         import std.typecons : nullableRef;
140         import colorlog : Color, color, Background, Mode;
141         import code_checker.engine.builtin.clang_tidy_classification : mapClangTidy;
142         import code_checker.process : exitCodeSegFault;
143 
144         auto res = nullableRef(cast() res_);
145 
146         logger.infof("%s/%s %s '%s'", cond.replies + 1, cond.expected,
147                 "clang-tidy analyzed".color(Color.yellow).bg(Background.black), res.file)
148             .collectException;
149 
150         result_.supp += res.suppressedWarnings;
151 
152         if (res.clangTidyStatus == 0) {
153             result_.success ~= res.file;
154         } else if (res.clangTidyStatus == exitCodeSegFault) {
155             res.print;
156             result_.msg ~= Msg(MsgSeverity.failReason, "clang-tidy segfaulted for " ~ res.file);
157         } else {
158             result_.score += res.errors.score;
159             result_.failed ~= res.file;
160             res.print;
161 
162             if (env.conf.logg.toFile) {
163                 try {
164                     logg.put(res.file, [res.output]);
165                 } catch (Exception e) {
166                     logger.warning(e.msg).collectException;
167                     logger.warning("Unable to log to file").collectException;
168                 }
169             }
170 
171             if (!logged_failure) {
172                 result_.msg ~= Msg(MsgSeverity.failReason, "clang-tidy warn about file(s)");
173                 logged_failure = true;
174             }
175 
176             try {
177                 result_.msg ~= Msg(MsgSeverity.improveSuggestion,
178                         format("clang-tidy: %-(%s, %) in %s", res.errors.toRange, res.file));
179             } catch (Exception e) {
180                 logger.warning(e.msg).collectException;
181                 logger.warning("Unable to add user message to the result").collectException;
182             }
183         }
184 
185         // by treating a segfault as OK it wont block a pull request. this may be a bad idea....
186         result_.status = mergeStatus(result_.status, res.clangTidyStatus.among(0,
187                 exitCodeSegFault) ? Status.passed : Status.failed);
188     }
189 
190     auto pool = new TaskPool;
191     scope (exit)
192         pool.finish;
193 
194     auto file_filter = FileFilter(env.conf.staticCode.fileExcludeFilter);
195     auto fixedDb = toRange(env);
196 
197     foreach (p; fixedDb) {
198         if (!exists(p.cmd.absoluteFile.toString)) {
199             result_.status = Status.failed;
200             result_.score -= 100;
201             result_.msg ~= Msg(MsgSeverity.failReason, "clang-tidy where unable to find one of the specified files in compile_commands.json on the filesystem. Your compile_commands.json is probably out of sync. Regenerate it.");
202             break;
203         } else if (!file_filter.match(p.cmd.absoluteFile)) {
204             if (logger.globalLogLevel == logger.LogLevel.all)
205                 result_.msg ~= Msg(MsgSeverity.trace,
206                         format("Skipping analyze because it didn't pass the file filter (user supplied regex): %s ",
207                             p.cmd.absoluteFile));
208         } else {
209             cond.expected++;
210 
211             immutable(TidyWork)* w = () @trusted {
212                 return cast(immutable) new TidyWork(tidyArgs, p.cmd.absoluteFile,
213                         !env.conf.logg.toFile, env.conf.staticCode.fileExcludeFilter);
214             }();
215             auto t = task!taskTidy(thisTid, w);
216             pool.put(t);
217         }
218     }
219 
220     while (cond.isWaitingForReplies) {
221         () @trusted {
222             try {
223                 if (receiveTimeout(1.dur!"seconds", &handleResult)) {
224                     cond.replies++;
225                 }
226             } catch (Exception e) {
227                 logger.error(e.msg);
228             }
229         }();
230     }
231 }
232 
233 /// Run clang-tidy with to fix the code.
234 void executeFixit(Environment env, string[] tidyArgs, ref Result result_) {
235     import code_checker.engine.logger : Logger;
236     import code_checker.engine.compile_db;
237 
238     auto logg = Logger(env.conf.logg.dir);
239 
240     if (env.conf.logg.toFile) {
241         logg.setup;
242         tidyArgs ~= [
243             "-export-fixes", buildPath(env.conf.logg.dir, "fixes.yaml")
244         ];
245     }
246 
247     void executeTidy(AbsolutePath file) {
248         auto args = tidyArgs ~ file;
249         logger.tracef("run: %s", args);
250 
251         auto status = spawnProcess(args).wait;
252         if (status == 0) {
253             result_.success ~= file;
254         } else {
255             result_.failed ~= file;
256             result_.status = Status.failed;
257             result_.score -= 100;
258             result_.msg ~= Msg(MsgSeverity.failReason, "clang-tidy failed to apply fixes for "
259                     ~ file ~ ". Use --clang-tidy-fix-errors to forcefully apply the fixes");
260         }
261     }
262 
263     auto file_filter = FileFilter(env.conf.staticCode.fileExcludeFilter);
264     auto fixedDb = toRange(env);
265 
266     const max_nr = fixedDb.length;
267     foreach (idx, cmd; fixedDb.enumerate) {
268         if (!file_filter.match(cmd.cmd.absoluteFile)) {
269             if (logger.globalLogLevel == logger.LogLevel.all)
270                 result_.msg ~= Msg(MsgSeverity.trace,
271                         format("Skipping analyze because it didn't pass the file filter (user supplied regex): %s ",
272                             cmd.cmd.absoluteFile));
273         } else {
274             logger.infof("File %s/%s %s", idx + 1, max_nr, cmd.cmd.absoluteFile);
275             executeTidy(cmd.cmd.absoluteFile);
276         }
277     }
278 }
279 
280 struct TidyResult {
281     AbsolutePath file;
282     CountErrorsResult errors;
283 
284     int suppressedWarnings;
285 
286     /// Exit status from running clang tidy
287     int clangTidyStatus;
288 
289     /// Output to the user
290     string[] output;
291 
292     void print() @safe nothrow const scope {
293         import std.ascii : newline;
294         import std.stdio : writeln;
295 
296         foreach (l; output)
297             writeln(l).collectException;
298     }
299 }
300 
301 struct TidyWork {
302     string[] args;
303     AbsolutePath p;
304     bool useColors;
305     string[] fileExcludeFilter;
306 }
307 
308 void taskTidy(Tid owner, immutable TidyWork* work_) nothrow @trusted {
309     import std.concurrency : send;
310     import std.format : format;
311     import code_checker.engine.builtin.clang_tidy_classification : mapClangTidy,
312         mapClangTidyStats, DiagMessage, StatMessage, color;
313 
314     auto tres = new TidyResult;
315     TidyWork* work = cast(TidyWork*) work_;
316 
317     void sendToOwner() {
318         while (true) {
319             try {
320                 owner.send(cast(immutable) tres);
321                 break;
322             } catch (Exception e) {
323                 logger.tracef("failed sending to: %s", owner).collectException;
324             }
325         }
326     }
327 
328     FileFilter file_filter;
329     try {
330         file_filter = FileFilter(work.fileExcludeFilter);
331     } catch (Exception e) {
332         logger.error(e.msg).collectException;
333         tres.clangTidyStatus = -1;
334         sendToOwner;
335         return;
336     }
337 
338     try {
339         // there may be warnings that are skipped. If all warnings are skipped
340         // and thus the counter is zero the result should be an automatic
341         // passed. This is because it means that all warnings where from a file
342         // that where excluded.
343         int count_errors;
344 
345         bool diagMsg(ref DiagMessage msg) {
346             if (!file_filter.match(msg.file))
347                 return false;
348 
349             count_errors++;
350             tres.errors.put(msg.severity);
351             if (work.useColors)
352                 msg.diagnostic = format("%s[%s]", msg.diagnostic, color(msg.severity));
353             else
354                 msg.diagnostic = format("%s[%s]", msg.diagnostic, msg.severity);
355             return true;
356         }
357 
358         void statMsg(StatMessage msg) {
359             tres.suppressedWarnings = msg.nolint;
360             tres.errors.setSuppressed(msg.nolint);
361         }
362 
363         tres.file = work.p;
364 
365         auto res = runClangTidy(work.args, [work.p]);
366 
367         auto app = appender!(string[])();
368         mapClangTidy!diagMsg(res.stdout, app);
369 
370         mapClangTidyStats!statMsg(res.stderr);
371 
372         tres.clangTidyStatus = res.status != 0 ? res.status : count_errors;
373 
374         if (tres.clangTidyStatus != 0) {
375             res.stderr.copy(app);
376             tres.output = app.data;
377         }
378     } catch (Exception e) {
379         logger.warning(e.msg).collectException;
380     }
381 
382     sendToOwner;
383 }
384 
385 struct ClangTidyConstants {
386     static immutable confFile = ".clang-tidy";
387     static immutable codeCheckerConfigHeader = "# GENERATED by code_checker";
388 }
389 
390 auto runClangTidy(string[] tidy_args, AbsolutePath[] fname) {
391     import code_checker.process;
392 
393     auto app = appender!(string[])();
394     tidy_args.copy(app);
395     fname.copy(app);
396 
397     auto rval = run(app.data);
398     if (rval.status == exitCodeSegFault)
399         return run(app.data);
400     return rval;
401 }
402 
403 bool isCodeCheckerConfig(AbsolutePath fname) @trusted nothrow {
404     import std.stdio : File;
405 
406     try {
407         foreach (l; File(fname).byLine) {
408             return l == ClangTidyConstants.codeCheckerConfigHeader;
409         }
410         return false;
411     } catch (Exception e) {
412         logger.trace(fname).collectException;
413         logger.trace(e.msg).collectException;
414     }
415 
416     return false;
417 }
418 
419 void writeClangTidyConfig(AbsolutePath baseConf, Config conf) @trusted {
420     import std.file : exists;
421     import std.stdio : File;
422     import std.ascii;
423     import std..string;
424     import code_checker.engine.builtin.clang_tidy_classification : filterSeverity;
425 
426     if (!exists(baseConf)) {
427         logger.warning("No default clang-tidy configuration found at ", baseConf);
428         logger.info("Using clang-tidy with default settings");
429         return;
430     }
431 
432     auto fconfig = File(ClangTidyConstants.confFile, "w");
433     fconfig.writeln(ClangTidyConstants.codeCheckerConfigHeader);
434 
435     string[] checks = () {
436         if (conf.staticCode.severity != typeof(conf.staticCode.severity).min)
437             return filterSeverity!(a => a < conf.staticCode.severity).map!(a => "-" ~ a).array;
438         return null;
439     }();
440 
441     if (checks.empty) {
442         foreach (d; File(baseConf).byChunk(4096))
443             fconfig.rawWrite(d);
444     } else {
445         enum State {
446             other,
447             checkKey,
448             openCheck,
449             insideCheck,
450             closeCheck,
451             afterCheck
452         }
453 
454         State st;
455         foreach (l; File(baseConf).byLine) {
456             auto curr = l;
457 
458             if (st == State.afterCheck) {
459                 fconfig.writeln(l);
460             } else {
461                 while (!curr.empty) {
462                     const auto old = st;
463                     final switch (st) {
464                     case State.other:
465                         if (curr.startsWith("Checks:")) {
466                             st = State.checkKey;
467                         } else {
468                             fconfig.write(curr[0]);
469                             curr = curr[1 .. $];
470                         }
471                         break;
472                     case State.checkKey:
473                         if (curr[0].among('"', '\'')) {
474                             st = State.openCheck;
475                         } else {
476                             fconfig.write(curr[0]);
477                             curr = curr[1 .. $];
478                         }
479                         break;
480                     case State.openCheck:
481                         fconfig.write(curr[0]);
482                         curr = curr[1 .. $];
483                         st = State.insideCheck;
484                         break;
485                     case State.insideCheck:
486                         if (curr[0].among('"', '\'')) {
487                             st = State.closeCheck;
488                         } else {
489                             fconfig.write(curr[0]);
490                             curr = curr[1 .. $];
491                         }
492                         break;
493                     case State.closeCheck:
494                         curr = curr[1 .. $];
495                         st = State.afterCheck;
496                         break;
497                     case State.afterCheck:
498                         fconfig.write(curr[0]);
499                         curr = curr[1 .. $];
500                         break;
501                     }
502 
503                     debug logger.tracef(old != st, "%s -> %s : %s", old, st, curr);
504 
505                     if (st == State.closeCheck) {
506                         fconfig.writeln(",\\");
507                         fconfig.write(checks.joiner(","));
508                         fconfig.write(curr[0]);
509                     }
510                 }
511 
512                 fconfig.writeln;
513             }
514         }
515         fconfig.writeln;
516     }
517 
518     foreach (kv; conf.clangTidy.optionExtensions.byKeyValue) {
519         fconfig.writeln("   - key:             ", kv.key);
520         fconfig.writefln("     value:           '%s'", kv.value);
521     }
522 }